/*==========================================================================*\
 |  $Id: Exceptions.java 9 2009-11-06 02:47:13Z VTWoods $
 |*-------------------------------------------------------------------------*|
 |  Copyright (C) 2009 Virginia Tech
 |
 |  This file is part of CloudSpace.
 |
 |  CloudSpace is free software; you can redistribute it and/or modify
 |  it under the terms of the GNU General Public License as published
 |  by the Free Software Foundation; either version 3 of the License, or
 |  (at your option) any later version.
 |
 |  CloudSpace is distributed in the hope that it will be useful,
 |  but WITHOUT ANY WARRANTY; without even the implied warranty of
 |  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 |  GNU General Public License for more details.
 |
 |  You should have received a copy of the GNU General Public License
 |  along with CloudSpace; if not, see <http://www.gnu.org/licenses/>.
\*==========================================================================*/

package student.web.internal;

import java.io.StringWriter;

import org.zkoss.zhtml.Pre;
import org.zkoss.zhtml.Text;
import org.zkoss.zk.ui.Component;
import org.zkoss.zul.Grid;
import org.zkoss.zul.Row;
import org.zkoss.zul.Rows;

import student.web.Window;

//-------------------------------------------------------------------------
/**
 *  A set of static methods for use by ZK infrastructure classes to
 *  extract information from and about exceptions.  Since all methods are
 *  static, the constructor is private.
 *
 *  @author Stephen Edwards
 *  @author Last changed by $Author: VTWoods $
 *  @version $Revision: 9 $, $Date: 2009-11-06 02:47:13 +0000 (Fri, 06 Nov 2009) $
 */
public class Exceptions
{
    //~ Constructor ...........................................................

    // ----------------------------------------------------------
    /**
     * This constructor is private, since this class only exports static
     * methods and should not be instantiated.
     */
    private Exceptions()
    {
        // Nothing to do but hide this constructor
    }


    //~ Methods ...............................................................

    // ----------------------------------------------------------
    /**
     * Drills down using {@link Throwable#getCause()} to find the root
     * cause of this (possibly wrapped) exception.
     * @param t The exception to inspect
     * @return The bottom-most exception contained within t, or t itself if
     * there is no nested or chained exception.
     */
    public static Throwable getRealCause(Throwable t)
    {
        return org.zkoss.lang.Exceptions.getRealCause(t);
    }


    // ----------------------------------------------------------
    /**
     * Get this exception's class name, without any package prefix.
     * @param t The exception to inspect
     * @return The class name without its package part
     */
    public static String simplifiedExceptionName(Throwable t)
    {
        String name = getRealCause(t).getClass().getName();
        int pos = name.lastIndexOf('.');
        if (pos >= 0)
        {
            name = name.substring(pos + 1);
        }
        return name;
    }


    // ----------------------------------------------------------
    /**
     * Generate a printable stack trace for the full set of stack frames
     * associated with this exception.  The trace looks similar to one
     * generated by {@link Throwable#printStackTrace()}, except that it
     * does not contain the exception name/message at the top and the
     * lines are not prefixed with tab characters.
     * @param t The exception to inspect
     * @return The full stack trace
     */
    public static String fullStackTrace(Throwable t)
    {
        StringWriter sw = new StringWriter();
        for (StackTraceElement frame : t.getStackTrace())
        {
            sw.append("at ");
            sw.append(frame.toString());
            sw.append("\n");
        }
        return sw.toString();
    }


    // ----------------------------------------------------------
    /**
     * Generate a simplified stack trace for this exception.  This is a
     * subset of the full stack trace where appropriate system-oriented
     * packages in upper levels of the stack have been omitted to focus
     * attention on the client code that led to the error.
     * @param t The exception to inspect
     * @return The stack trace.  May return an empty string if no client
     * or course-specific classes can be found anywhere in the exception's
     * stack frames.
     * @see #skipClass(String)
     * @see #courseInfrastructureClass(String)
     */
    public static String simplifiedStackTrace(Throwable t)
    {
        int topSystemFrame = -1;
        int topUserFrame = -1;
        StackTraceElement[] frames = t.getStackTrace();
        for (int i = frames.length - 1; i > 0; i--)
        {
            String name = frames[i].getClassName();
            if (!skipClass(name))
            {
                if (topSystemFrame < i)
                {
                    topSystemFrame = i;
                }
                if (!courseInfrastructureClass( name ))
                {
                    topUserFrame = i;
                    break;
                }
            }
        }
        if (topSystemFrame < 0)
        {
            return null;
        }
        StringWriter sw = new StringWriter();
//        sw.append("topUserFrame = " + topUserFrame + "\n"
//            + "topSystemFrame = " + topSystemFrame + "\n"
//            );
        for (int i = 0; i <= topSystemFrame; i++)
        {
            if (i <= topUserFrame || !skipClass(frames[i].getClassName()))
            {
                sw.append("at ");
                sw.append(frames[i].toString());
                sw.append("\n");
            }
        }
        return sw.toString();
    }


    // ----------------------------------------------------------
    /**
     * Try to generate a simplified stack trace for this exception, or
     * generate a full trace if no simple trace can be created.
     * @param t The exception to inspect
     * @return The stack trace
     */
    public static String bestStackTrace(Throwable t)
    {
        String result = simplifiedStackTrace( t );
        if ( result == null )
        {
            result = fullStackTrace( t );
        }
        return result;
    }


    // ----------------------------------------------------------
    /**
     * Tests a class name to see if it should be considered non-client,
     * non-course-specific code that should be omitted from higher levels
     * of simplified stack traces.
     * @param className The name to check
     * @return True if this class is not a client or course class
     */
    public static boolean skipClass(String className)
    {
        return className.startsWith("java.")
            || className.startsWith("javax.")
            || className.startsWith("org.zkoss.")
            || className.startsWith("sun.")
            || className.startsWith("com.sun.")
            || className.startsWith("org.apache.")
            || className.startsWith("bsh.");
    }


    // ----------------------------------------------------------
    /**
     * Tests a class name to see if it should be considered as
     * course-specific, non-client code.  Such classes are always included
     * in simplified stack traces, but they do not mark the beginning of
     * client code stack frames.
     * @param className The name to check
     * @return True if this class is a course class
     */
    public static boolean courseInfrastructureClass(String className)
    {
        return className.startsWith("student.")
            || className.startsWith("cs1705.");
    }


    // ----------------------------------------------------------
    /**
     * Add a grid component with exception details and a stack trace
     * to a ZK page.
     * @param component The component to page the grid on as a child
     * @param t The exception to generate details for
     * @param location An optional string indicating where the exception
     * occurred or what was being attempted, purely for user information.  If
     * null, it will be omitted from the grid.
     */
    public static void addSimpleExceptionGrid(
        Component component,
        Throwable t,
        String    location)
    {
        addSimpleExceptionGrid(component, t, location, true);
    }


    // ----------------------------------------------------------
    /**
     * Add a grid component with exception details to a ZK page.
     * @param component The component to page the grid on as a child
     * @param t The exception to generate details for
     * @param location An optional string indicating where the exception
     * occurred or what was being attempted, purely for user information.  If
     * null, it will be omitted from the grid.
     * @param showStackTrace Whether or not the exception's stack trace should
     * be shown in the grid.
     */
    public static void addSimpleExceptionGrid(
        Component component,
        Throwable t,
        String    location,
        boolean   showStackTrace)
    {
        t = getRealCause(t);
        Window window = new Window("Unexpected Exception", "normal", false);
        window.setContentStyle("padding:0;margin:0");
        component.appendChild(window);
        Component grid = new Grid();
        window.appendChild(grid);
        Component rows = new Rows();
        grid.appendChild(rows);
        Component row = null;

        if (location != null)
        {
            row = new Row();
            rows.appendChild(row);
            row.appendChild(new Text("Location:"));
            row.appendChild(new Text(location));
        }
        row = new Row();
        rows.appendChild(row);
        row.appendChild(new Text("Exception:"));
        row.appendChild(new Text(t.getClass().getName()));

        if (t.getMessage() != null)
        {
            String message = escapeHtml(t.getMessage());
            if (t instanceof bsh.EvalError )
            {
                message = message.replaceAll("((^| : )([^:]+)):",
                      "$2<b>$3</b>:").replaceAll(" : ", "<br/>");
            }

            row = new Row();
            rows.appendChild(row);
            row.appendChild(new Text("Message:"));
            row.appendChild(new Text(message));
        }

        if (showStackTrace)
        {
            String trace = simplifiedStackTrace(t);
            if (trace != null)
            {
                row = new Row();
                rows.appendChild(row);
                row.appendChild(new Text("Stack trace:"));
                Component pre = new Pre();
                row.appendChild(pre);
                pre.appendChild(new Text(trace));
            }
        }
    }


    // ----------------------------------------------------------
    /**
     * Escape the HTML special characters in a string.  This means replacing
     * the following:
     * <ul>
     * <li>&amp; by &amp;amp;</li>
     * <li>&lt; by &amp;lt;</li>
     * <li>&gt; by &amp;gt;</li>
     * <li>&#034; by &amp;#034;</li>
     * <li>&#039; by &amp;#039;</li>
     * </ul>
     * @param str The string to translate
     * @return The corresponding string with HTML special characters
     * substituted by their escape sequences.
     */
    public static String escapeHtml(String str)
    {
        if (str != null)
        {
            str = str
                .replaceAll("&",  "&amp;")
                .replaceAll("<",  "&lt;")
                .replaceAll(">",  "&gt;")
                .replaceAll("\"", "&#034;")
                .replaceAll("'",  "&#039;");
        }
        return str;
    }
}
